package com.zzj.ecology.framework.configs.security;

import com.zzj.ecology.commons.utils.JWTUtils;
import com.zzj.ecology.framework.configs.security.filter.VeriryCodeFilter;
import com.zzj.ecology.framework.configs.security.handler.CustomizeAccessDeniedHandler;
import com.zzj.ecology.framework.configs.security.handler.CustomizeAuthenticationFailureHandler;
import com.zzj.ecology.framework.configs.security.handler.CustomizeAuthenticationSuccessHandler;
import com.zzj.ecology.framework.configs.security.handler.CustomizeLogoutSuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.sql.DataSource;

/**
 * https://www.cnblogs.com/xuwenjin/p/9933218.html
 *
 * https://www.cnblogs.com/lihaoyang/p/8507889.html
 */
@Configuration
@EnableWebSecurity
//@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启 Security 注解
public class CustomizingSpringSceurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private DataSource dataSource; // 数据源

    /**
     * 处理匿名访问的异常处理
     */
    @Autowired
    private CustomizeAuthenticationEntryPoint customizeAuthenticationEntryPoint;

    /**
     * 登入成功 处理器
     */
    @Autowired
    private CustomizeAuthenticationSuccessHandler customizeAuthenticationSuccessHandler;
    /**
     * 登入失败 处理器
     */
    @Autowired
    private CustomizeAuthenticationFailureHandler customizeAuthenticationFailureHandler;

    /**
     * 登出成功 处理器
     */
    @Autowired
    private CustomizeLogoutSuccessHandler customizeLogoutSuccessHandler;

    /**
     * 权限不足 处理器
     */
    @Autowired
    private CustomizeAccessDeniedHandler customizeAccessDeniedHandler;

    /**
     * SESSION 异地同时登录时的策略
     */
    @Autowired
    private CustomizeSessionInformationExpiredStrategy customizeSessionInformationExpiredStrategy;

    /**
     * 验证码 Filter
     */
    @Autowired
    private VeriryCodeFilter veriryCodeFilter;

    public CustomizingSpringSceurityConfiguration() { }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //super.configure(auth);
        //注入自定义的UserDetailsService
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        /* 开启跨域共享    跨域伪造请求限制=无效 */
        http.cors().and().csrf().disable();

        /**
         * 页面报一个"Refused to display 'http://......' in a frame because it set 'X-Frame-Options' to 'DENY'. "错误
         * 需要配置如下, 在 IFRAME 中打开页面时方不会报上述错误
         */
        http.headers().frameOptions().disable();
        //X-Content-Type-Options 头设置允许加载静态资源文件，处理以下问题
        //来自“http://localhost:8080/api.js”的资源已被阻止，因为 MIME 类型（“application/json”）不匹配（X-Content-Type-Options: nosniff）。
        http.headers().contentTypeOptions().disable();

        /* 添加自定义 Filter 此处添加验证码过滤器 */
        http.addFilterBefore(veriryCodeFilter, UsernamePasswordAuthenticationFilter.class);

        http.authorizeRequests().antMatchers("/test/**").permitAll();

        /* 创建 UsernamePasswordAuthenticationFilter username 和 password 的登录页面 */
        // 不使用http.formLogin()默认登录的情况下,使用http.exceptionHandling().authenticationEntryPoint(loginUrlAuthenticationEntryPoint())  //使用新的登录EntryPoint
        // LoginUrlAuthenticationEntryPoint，继续使用 http.formLogin() 也可以
        http.formLogin()
                .usernameParameter("username")
                .passwordParameter("password")
                .loginProcessingUrl("/login")// 默认处理登录的请求/login, 可以此处修改,指定自己的登录action
                //.loginPage("/static/login.html")    // 登录的页面
                //.defaultSuccessUrl("/")      // 成功登录的跳转请求
                /* 登录成功Handler */
                .successHandler(customizeAuthenticationSuccessHandler)
                /* 登录失败的处理器 */
                .failureHandler(customizeAuthenticationFailureHandler);

        http.exceptionHandling()
                /* 配置登录超时或者未登录处理器 */
                .authenticationEntryPoint(customizeAuthenticationEntryPoint)
                /* 配置权限不足处理器 */
                .accessDeniedHandler(customizeAccessDeniedHandler);

        /* 登出处理器 并删除 SESSION  */
        http.logout()
                .logoutSuccessHandler(customizeLogoutSuccessHandler)
                .deleteCookies("JSESSIONID")
                .invalidateHttpSession(true);//用户的HTTP session将会在退出时被失效。在一些场景下，这是必要的（如用户拥有一个购物车时）

        /* 限制 同一个账号只能一个用户使用 */
        http.sessionManagement()
                .maximumSessions(1)//同一账号同时登录最大用户数
                .expiredSessionStrategy(customizeSessionInformationExpiredStrategy);//SESSION失效(账号被挤下线)处理逻辑

        /* 记住我功能, 存库并返回JSESSIONID, TOKEN 有效期内直接登录 */
        http.rememberMe().rememberMeParameter("rememberme")
                        //.alwaysRemember(true)//不管前端有没有勾选，均认为是勾选
                        .userDetailsService(userDetailsService)
                        .tokenRepository(persistentTokenRepository())//设置数据访问层
                        .tokenValiditySeconds(Integer.valueOf(JWTUtils.EXPIRE/1000 + ""));//7 * 24 * 60 * 60

    }


    /**
     * 静态资源 地址过滤
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers("/ghdg/**");// URL 以 /static/ 开头的过滤
        web.ignoring().antMatchers("/ghdg/**/*.js");
        web.ignoring().antMatchers("/ghdg/**/*.css");
    }

    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }



    /**
     * 持久化token
     *
     * Security中，默认是使用PersistentTokenRepository的子类InMemoryTokenRepositoryImpl，将token放在内存中
     * 如果使用JdbcTokenRepositoryImpl，会创建表persistent_logins，将token持久化到数据库
     *
     * 如果数据库中无 persistent_logins 表的话，报下述异常
     * org.springframework.jdbc.BadSqlGrammarException: PreparedStatementCallback;
     *        bad SQL grammar [insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)];
     *        nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException:
     *        Table 'fapd_business.persistent_logins' doesn't exist
     *
     * 手动创建 persistent_logins 具有 username/series/token/last_used DATE 四个字段,前面三个是varchar,后面是date类型。
     *
     * create table persistent_logins(username varchar , series varchar, token varchar, last_used date);...
     * 用户成功登录并记住我时会在上述表中写入一条记录,如果成功登出会删除此记录。username是登录的账号。
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource); // 设置数据源
//        tokenRepository.setCreateTableOnStartup(true); // 启动创建表，第一次创建表时使用，之后注释(建议查询原代码，直接手动创建表)
        return tokenRepository;
    }


}
